﻿/*
   Copyright 2011 Softisis

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.Collections;
using System.Collections.Generic;
using FOM.Interfaces;

namespace FOM.Impl.Converters
{
    /// <summary>
    /// This converter may be used for array conversion. This is usefull to transform fixed length array
    /// into a List for example.
    /// </summary>
    public class ListCrossConv : IConverter
    {
        private readonly IEnumerable<INode> _subMappings;


        /// <summary>
        /// Basic contructor
        /// </summary>
        public ListCrossConv()
        {
            
        }

        /// <summary>
        /// Constructor providing submapping for object in list. Because non generic List can contain multiple
        /// types, multiples sub-mappings are allowed, but one per exact type.
        /// Matching is done on the first type of mapping rule.
        /// Is you use ordered list, you can manage the subMapping priority this way.
        /// </summary>
        /// <param name="subMappings">Submappings. Only one source type is allowed.</param>
        public ListCrossConv(IEnumerable<INode> subMappings)
        {
            _subMappings = subMappings;
        }

        /// <summary>
        /// Constructor providing one submapping that will be applied to source objects in list.
        /// Objects in list must be instances of sourceType of the mapping.
        /// Otherwise, no sub-process is done, an objects are returned "as it".
        /// </summary>
        /// <param name="subMapping">The sub mapping to apply to objects in source</param>
        public ListCrossConv(INode subMapping)
        {
            _subMappings = new List<INode>();
            (_subMappings as List<INode>).Add(subMapping);
        }

        /// <summary>
        /// Apply the mapping for each sub element of the array. the matching is done:
        /// Priority is done by Enumeration of the source. Is source is ordered, matching
        /// rules can be managed this way.
        /// </summary>
        protected object ApplyElementSubMappingIfNeeded(object source, MappingContext ctx)
        {
            if (_subMappings == null) // no processing
                return source;
            if (source == null)
                return null;

            foreach (INode subMapping in _subMappings)
            {
                AbstractMappingNode realsm = subMapping as AbstractMappingNode;
                IProxyNode pxynode = subMapping as IProxyNode;
                if (realsm==null && pxynode!=null)
                    realsm = pxynode.BaseNode;
                
                if (realsm==null)
                    throw new Exception("FOM Node type not supported:" + subMapping.GetType());

                var srctype = realsm.SourceType;
                if (srctype.IsInstanceOfType(source))
                    return ctx.Manager.ApplyMapping((INode) subMapping, source, source, ctx.ObjectsCache);
                        // we keep the source by default
            }
            return source;

        }

        /// <summary>
        /// Convert. This method will do its best to take objects from one list to another one. Rules are the following:
        /// - HashSet, Array, List to HashSet, Array, List: One to one copy. Be carefull with Hashet that must can't contain the same values more than once.
        /// - HashSet, Array, List to IDictionnary: source Values are repeated in Key and Value.
        /// - IDictionnary to HashSet, Array, List: Well ... Only values from the dictionnary are copied.
        /// - IDictionnary to IDictionnary: ISO copy.
        /// </summary>
        /// <returns>The converted "List"</returns>
        public object Convert(MappingContext ctx)
        {
            if (ctx.ObjectSrc == null)
                return null;

            var srcenum = ctx.ObjectSrc as IEnumerable;
            if (srcenum == null)
                throw new Exception("Source object must be an IEnumerable.");

            bool cacheowner = false;
            if (ctx.IsNewInstance && ctx.IsNewinstanceUseRef)
            {
                // we put object in cache because engine do not have THIS list in cache.
                // cache is put after the conversion. So we fake it here.
                if (!ctx.ObjectsCache.ContainsKey(ctx.ObjectSrc))
                {
                    cacheowner = true;
                    ctx.ObjectsCache[ctx.ObjectSrc] = ctx.ObjectDst;
                }
            }
            try
            {
                IList dstaslist = ctx.ObjectDst as IList;
                IDictionary dstasdic = ctx.ObjectDst as IDictionary;

                /******* ARRAY ******/
                if (ctx.ObjectDst.GetType().IsArray)
                {
                    // lenth is supposed to be ok
                    int i = 0;
                    var tmparr = (Array) ctx.ObjectDst;
                    foreach (var v in srcenum)
                    {
                        tmparr.SetValue(ApplyElementSubMappingIfNeeded(v, ctx), i);
                        i++;
                    }
                }
                    /**** HASHSET *****/
                    // hashset must have special treatment as it's only generic
                else if (ctx.ObjectDst.GetType().IsGenericType &&
                         ctx.ObjectDst.GetType().GetGenericTypeDefinition() == typeof (HashSet<>))
                {
                    var meth = ctx.ObjectDst.GetType().GetMethod("Add");
                    foreach (var va in srcenum)
                        // For info: Copying a IDictionnary to IList is not forbiden, but can lead to strange behaviour
                    {
                        object[] tmp = {ApplyElementSubMappingIfNeeded(va, ctx)};
                        meth.Invoke(ctx.ObjectDst, tmp);
                            // invokation. We can't cast to a generic without having the real class name
                    }
                }
                    /****** ILIST *******/
                else if (dstaslist != null) // type IList
                {
                    foreach (var va in srcenum)
                        dstaslist.Add(ApplyElementSubMappingIfNeeded(va, ctx));
                }
                    /****** IDICTIONNARY ******/
                else if (dstasdic != null) // Type IDictionnary
                {
                    // Check if source is a IDictionnary as well
                    IDictionary srcenumasidic = ctx.ObjectSrc as IDictionary;
                    if (srcenumasidic != null)
                    {
                        foreach (var v2 in srcenumasidic)
                        {
                            var tgtelt = ApplyElementSubMappingIfNeeded(v2, ctx);
                            dstasdic[tgtelt] = srcenumasidic[tgtelt];
                        }
                    }
                    // sinon mode degradé
                    foreach (var vb in srcenum)
                    {
                        var tgtelt = ApplyElementSubMappingIfNeeded(vb, ctx);
                        dstasdic.Add(tgtelt, tgtelt); // clef et valeur identiques
                    }
                }
                else
                {
                    throw new Exception(
                        "Target can't be copied because it does not support any recognized 'add' or indexation method");
                }
            }finally
            {
                // remove obj from cache "faked"
                if (cacheowner) // only those who put n the cache have to remove it
                    ctx.ObjectsCache.Remove(ctx.ObjectSrc);

            }

            return ctx.ObjectDst;
        }

        public IConverter Reverse()
        {
            return this;
        }
    }
}
